﻿
namespace Patterns4Net.ArchitectureExplorer.Diagram.Services
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;
    using Caliburn.Micro;
    using Mono.Cecil;
    using Mono.Cecil.Rocks;
    using Documentation;
    using Infrastructure;
    using Core;
    using Core.CecilExtensions;
    using Attributes.Diagrams;

    [Export(typeof(IDiagramLoader))]
    public class CecilDiagramLoader : IDiagramLoader
    {
        private readonly IEnumerable<Lazy<IDiagramUpdater, IDiagramUpdaterMetadata>> diagramUpdaters;

        private readonly ApiDocProvider apiDocProvider;

        private readonly Settings settings;

        private readonly IList<string> loadedAssemblies = new List<string>();

        /// <summary>
        /// If the diagram changes between invocations of <see cref="Load"/>, then 
        /// <see cref="loadedAssemblies"/> are cleared.
        /// </summary>
        private Diagram lastDiagram;

        private IDictionary<string, Regex> epressions = new Dictionary<string, Regex>();

        [ImportingConstructor]
        public CecilDiagramLoader(
            [ImportMany]IEnumerable<Lazy<IDiagramUpdater, IDiagramUpdaterMetadata>> diagramUpdaters,
            ApiDocProvider apiDocProvider,
            Settings settings)
        {
            this.diagramUpdaters = diagramUpdaters;
            this.apiDocProvider = apiDocProvider;
            this.settings = settings;
            this.Logger = new NullLogger();
        }

        [Import]
        public ILog Logger { get; set; }

        public void Load(string filename, Diagram diagram, TextWriter output)
        {
            if (diagram != this.lastDiagram)
            {
                this.lastDiagram = diagram;
                this.loadedAssemblies.Clear();
            }

            if (this.loadedAssemblies.Contains(filename))
            {
                output.WriteLine("Assembly {0} has been already loaded. Skipping this assembly...", filename);
                return;
            }

            output.WriteLine("Loading assembly {0} ...", Path.GetFileName(filename));
            AssemblyDefinition assembly;
            try
            {
                assembly = AssemblyDefinition.ReadAssembly(filename);
            }
            catch (Exception ex)
            {
                output.WriteLine(
                    "CecilDiagramLoader cannot load assembly {0}, because an unexpected error {1} occurred. " +
                    "Details of the error have been logged.",
                    filename,
                    ex.GetType().Name);
                this.Logger.Error(ex);
                return;
            }

            this.loadedAssemblies.Add(filename);
            this.apiDocProvider.AddAssembly(assembly, filename);
            AssembliesList assembliesList;
            if (diagram.TryGetTag<AssembliesList>(out assembliesList))
            {
                assembliesList.Add(assembly);
            }
            else
            {
                diagram.AddTag(new AssembliesList { assembly });
            }

            var types = assembly.Modules.SelectMany(module => module.GetAllTypes());

            if (assembly.HasCustomAttributes &&
                assembly.HasAttribute<LayerAttribute>())
            {
                var layers = assembly.CustomAttributes.Where(attr => attr.AttributeType.IsEqual(typeof(LayerAttribute)));
                foreach (var layer in layers)
                {
                    Contract.Assert(layer.ConstructorArguments.Count == 1, "LayerAttribute should have one ctor argument.");
                    string @namespace;
                    if (layer.ConstructorArguments.First().Type.IsEqual(typeof(string)))
                    {
                        @namespace = (string)layer.ConstructorArguments.First().Value;
                    }
                    else if (layer.ConstructorArguments.First().Type.IsEqual(typeof(Type)))
                    {
                        @namespace = ((TypeReference)layer.ConstructorArguments.First().Value).Namespace;
                    }
                    else
                    {
                        Contract.Assert(false, "LayerAttribute should have ctor argument of type string of System.Type.");
                        continue;
                    }

                    var name = GetNamespaceName(@namespace);
                    this.SafeLoadLayer(name, types.Where(t => IsInNamespace(t, @namespace)), diagram, output);
                }
            }
            else
            {
                this.SafeLoadLayer(assembly.Name.Name, types, diagram, output);
            }

            foreach (var updater in diagramUpdaters.OrderBy(x => x.Metadata.Order).Select(x => x.Value))
            {
                updater.UpdateDiagram(diagram, output);
            }   
        }

        private void SafeLoadLayer(string name, IEnumerable<TypeDefinition> types, Diagram diagram, TextWriter output)
        {
            try
            {
                this.LoadLayer(name, types, diagram);
                output.WriteLine(
                    "Layer {0} was successfully loaded. ({1} types in {2} packages found) ...",
                    name,
                    diagram.Layers.Last().Packages.Sum(p => p.Types.Count()),
                    diagram.Layers.Last().Packages.Count());
            }
            catch (Exception ex)
            {
                output.WriteLine(
                    "CecilDiagramLoader cannot load layer {0}, because an unexpected error occurred. " +
                    "Error details have been logged.",
                    name);
                this.Logger.Error(ex);
            }
        }

        private void LoadLayer(string name, IEnumerable<TypeDefinition> types, Diagram diagram)
        {            
            var typeInfos = types
                .Where(type => this.IsValidType(type.Name))
                .Select(
                    typeDefinition =>
                    {
                        var typeInfo = new TypeInfo(
                            GetName(typeDefinition),
                            GetProperties(typeDefinition),
                            GetMethods(typeDefinition));
                        typeInfo.AddTag(typeDefinition);
                        return typeInfo;
                    });

            var packages = new Dictionary<string, IList<TypeInfo>>();
            foreach (var typeInfo in typeInfos)
            {
                IList<TypeInfo> packageTypes;
                var @namespace = GetNamespace(typeInfo.GetTag<TypeDefinition>());
                if (packages.TryGetValue(@namespace, out packageTypes))
                {
                    packageTypes.Add(typeInfo);
                }
                else
                {
                    packageTypes = new List<TypeInfo>();
                    packageTypes.Add(typeInfo);
                    packages[@namespace] = packageTypes;
                }
            }

            diagram.AddLayer(
                new Layer(
                    name,
                    packages.Keys.Where(IsValidNamespace).Select(
                        @namespace =>
                        new Package(GetNamespaceName(@namespace),
                            packages.Where(x => x.Key == @namespace).SelectMany(x => x.Value))).ToArray()));
        }

        private bool IsValidNamespace(string name)
        {
            return string.IsNullOrWhiteSpace(name) == false &&
                this.settings.IgnoredNamespaces.None(name.StartsWith);
        }

        private bool IsValidType(string name)
        {
            return this.settings.IgnoredTypes.None(x => this.GetRegex(x).IsMatch(name));
        }

        private bool IsValidMember(string name)
        {
            return this.settings.IgnoredTypes.None(x => this.GetRegex(x).IsMatch(name));
        }

        private Regex GetRegex(string expression)
        {
            Regex result;
            if (this.epressions.TryGetValue(expression, out result) == false)
            {
                try
                {
                    result = new Regex(expression);
                    this.epressions.Add(expression, result);
                }
                catch
                {
                    // the expression is not well formed, this happens only when it was 
                    // inserted directly into the configuration file. Settings dialog 
                    // validates regular expressions.
                }
            }

            return result;
        }

        private static string GetName(TypeDefinition type)
        {
            if (type.IsNested)
            {
                return GetName(type.DeclaringType) + "." + type.Name;
            }

            return type.Name;
        }

        private static bool IsInNamespace(TypeDefinition type, string @namespace)
        {
            return GetNamespace(type).StartsWith(@namespace);
        }

        /// <summary>
        /// Returns correct namespace of normal but also nested types.
        /// </summary>
        private static string GetNamespace(TypeDefinition type)
        {
            if (type.IsNested)
            {
                return GetNamespace(type.DeclaringType);
            }

            return type.Namespace;
        }

        private static string GetNamespaceName(string @namespace)
        {
            int dotIndex = @namespace.LastIndexOf(".");
            if (dotIndex == -1)
            {
                return @namespace;
            }

            return @namespace.Substring(dotIndex + 1, @namespace.Length - dotIndex - 1);
        }

        private IEnumerable<PropertyInfo> GetProperties(TypeDefinition type)
        {
            foreach (var property in type.Properties.Where(p => this.IsValidMember(p.Name)))
            {
                var info = new PropertyInfo(property.Name, property.PropertyType.Name);
                info.AddTag(property);
                yield return info;
            }
        }

        private IEnumerable<MethodInfo> GetMethods(TypeDefinition type)
        {
            foreach (var method in type.GetMethods().Where(this.MethodsFilter))
            {
                var info = new MethodInfo(method.Name, method.Parameters.Select(p => p.Name));
                info.AddTag(method);
                yield return info;
            }
        }

        private bool MethodsFilter(MethodDefinition method)
        {
            return method.IsGetter == false &&
                   method.IsSetter == false &&
                   method.IsConstructor == false &&
                   this.IsValidMember(method.Name);
        }

        public class AssembliesList : List<AssemblyDefinition>
        {            
        }
    }
}
